home *** CD-ROM | disk | FTP | other *** search
/ Freelog 125 / Freelog_MarsAvril2015_No125.iso / ViePratique / ArchiFacile / ArchiFacileSetup.exe / {app} / nw.pak / Unnamed File 000125.txt < prev    next >
Text File  |  2014-10-14  |  43KB  |  1,327 lines

  1. // Copyright (c) 2012 The Chromium Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style license that can be
  3. // found in the LICENSE file.
  4.  
  5. // require: array_data_model.js
  6. // require: list_selection_model.js
  7. // require: list_selection_controller.js
  8. // require: list_item.js
  9.  
  10. /**
  11.  * @fileoverview This implements a list control.
  12.  */
  13.  
  14. cr.define('cr.ui', function() {
  15.   /** @const */ var ListSelectionModel = cr.ui.ListSelectionModel;
  16.   /** @const */ var ListSelectionController = cr.ui.ListSelectionController;
  17.   /** @const */ var ArrayDataModel = cr.ui.ArrayDataModel;
  18.  
  19.   /**
  20.    * Whether a mouse event is inside the element viewport. This will return
  21.    * false if the mouseevent was generated over a border or a scrollbar.
  22.    * @param {!HTMLElement} el The element to test the event with.
  23.    * @param {!Event} e The mouse event.
  24.    * @param {boolean} Whether the mouse event was inside the viewport.
  25.    */
  26.   function inViewport(el, e) {
  27.     var rect = el.getBoundingClientRect();
  28.     var x = e.clientX;
  29.     var y = e.clientY;
  30.     return x >= rect.left + el.clientLeft &&
  31.            x < rect.left + el.clientLeft + el.clientWidth &&
  32.            y >= rect.top + el.clientTop &&
  33.            y < rect.top + el.clientTop + el.clientHeight;
  34.   }
  35.  
  36.   function getComputedStyle(el) {
  37.     return el.ownerDocument.defaultView.getComputedStyle(el);
  38.   }
  39.  
  40.   /**
  41.    * Creates a new list element.
  42.    * @param {Object=} opt_propertyBag Optional properties.
  43.    * @constructor
  44.    * @extends {HTMLUListElement}
  45.    */
  46.   var List = cr.ui.define('list');
  47.  
  48.   List.prototype = {
  49.     __proto__: HTMLUListElement.prototype,
  50.  
  51.     /**
  52.      * Measured size of list items. This is lazily calculated the first time it
  53.      * is needed. Note that lead item is allowed to have a different height, to
  54.      * accommodate lists where a single item at a time can be expanded to show
  55.      * more detail.
  56.      * @type {{height: number, marginTop: number, marginBottom:number,
  57.      *     width: number, marginLeft: number, marginRight:number}}
  58.      * @private
  59.      */
  60.     measured_: undefined,
  61.  
  62.     /**
  63.      * Whether or not the list is autoexpanding. If true, the list resizes
  64.      * its height to accomadate all children.
  65.      * @type {boolean}
  66.      * @private
  67.      */
  68.     autoExpands_: false,
  69.  
  70.     /**
  71.      * Whether or not the rows on list have various heights. If true, all the
  72.      * rows have the same fixed height. Otherwise, each row resizes its height
  73.      * to accommodate all contents.
  74.      * @type {boolean}
  75.      * @private
  76.      */
  77.     fixedHeight_: true,
  78.  
  79.     /**
  80.      * Whether or not the list view has a blank space below the last row.
  81.      * @type {boolean}
  82.      * @private
  83.      */
  84.     remainingSpace_: true,
  85.  
  86.     /**
  87.      * Function used to create grid items.
  88.      * @type {function(): !ListItem}
  89.      * @private
  90.      */
  91.     itemConstructor_: cr.ui.ListItem,
  92.  
  93.     /**
  94.      * Function used to create grid items.
  95.      * @type {function(): !ListItem}
  96.      */
  97.     get itemConstructor() {
  98.       return this.itemConstructor_;
  99.     },
  100.     set itemConstructor(func) {
  101.       if (func != this.itemConstructor_) {
  102.         this.itemConstructor_ = func;
  103.         this.cachedItems_ = {};
  104.         this.redraw();
  105.       }
  106.     },
  107.  
  108.     dataModel_: null,
  109.  
  110.     /**
  111.      * The data model driving the list.
  112.      * @type {ArrayDataModel}
  113.      */
  114.     set dataModel(dataModel) {
  115.       if (this.dataModel_ != dataModel) {
  116.         if (!this.boundHandleDataModelPermuted_) {
  117.           this.boundHandleDataModelPermuted_ =
  118.               this.handleDataModelPermuted_.bind(this);
  119.           this.boundHandleDataModelChange_ =
  120.               this.handleDataModelChange_.bind(this);
  121.         }
  122.  
  123.         if (this.dataModel_) {
  124.           this.dataModel_.removeEventListener(
  125.               'permuted',
  126.               this.boundHandleDataModelPermuted_);
  127.           this.dataModel_.removeEventListener('change',
  128.                                               this.boundHandleDataModelChange_);
  129.         }
  130.  
  131.         this.dataModel_ = dataModel;
  132.  
  133.         this.cachedItems_ = {};
  134.         this.cachedItemHeights_ = {};
  135.         this.selectionModel.clear();
  136.         if (dataModel)
  137.           this.selectionModel.adjustLength(dataModel.length);
  138.  
  139.         if (this.dataModel_) {
  140.           this.dataModel_.addEventListener(
  141.               'permuted',
  142.               this.boundHandleDataModelPermuted_);
  143.           this.dataModel_.addEventListener('change',
  144.                                            this.boundHandleDataModelChange_);
  145.         }
  146.  
  147.         this.redraw();
  148.       }
  149.     },
  150.  
  151.     get dataModel() {
  152.       return this.dataModel_;
  153.     },
  154.  
  155.  
  156.     /**
  157.      * Cached item for measuring the default item size by measureItem().
  158.      * @type {ListItem}
  159.      */
  160.     cachedMeasuredItem_: null,
  161.  
  162.     /**
  163.      * The selection model to use.
  164.      * @type {cr.ui.ListSelectionModel}
  165.      */
  166.     get selectionModel() {
  167.       return this.selectionModel_;
  168.     },
  169.     set selectionModel(sm) {
  170.       var oldSm = this.selectionModel_;
  171.       if (oldSm == sm)
  172.         return;
  173.  
  174.       if (!this.boundHandleOnChange_) {
  175.         this.boundHandleOnChange_ = this.handleOnChange_.bind(this);
  176.         this.boundHandleLeadChange_ = this.handleLeadChange_.bind(this);
  177.       }
  178.  
  179.       if (oldSm) {
  180.         oldSm.removeEventListener('change', this.boundHandleOnChange_);
  181.         oldSm.removeEventListener('leadIndexChange',
  182.                                   this.boundHandleLeadChange_);
  183.       }
  184.  
  185.       this.selectionModel_ = sm;
  186.       this.selectionController_ = this.createSelectionController(sm);
  187.  
  188.       if (sm) {
  189.         sm.addEventListener('change', this.boundHandleOnChange_);
  190.         sm.addEventListener('leadIndexChange', this.boundHandleLeadChange_);
  191.       }
  192.     },
  193.  
  194.     /**
  195.      * Whether or not the list auto-expands.
  196.      * @type {boolean}
  197.      */
  198.     get autoExpands() {
  199.       return this.autoExpands_;
  200.     },
  201.     set autoExpands(autoExpands) {
  202.       if (this.autoExpands_ == autoExpands)
  203.         return;
  204.       this.autoExpands_ = autoExpands;
  205.       this.redraw();
  206.     },
  207.  
  208.     /**
  209.      * Whether or not the rows on list have various heights.
  210.      * @type {boolean}
  211.      */
  212.     get fixedHeight() {
  213.       return this.fixedHeight_;
  214.     },
  215.     set fixedHeight(fixedHeight) {
  216.       if (this.fixedHeight_ == fixedHeight)
  217.         return;
  218.       this.fixedHeight_ = fixedHeight;
  219.       this.redraw();
  220.     },
  221.  
  222.     /**
  223.      * Convenience alias for selectionModel.selectedItem
  224.      * @type {*}
  225.      */
  226.     get selectedItem() {
  227.       var dataModel = this.dataModel;
  228.       if (dataModel) {
  229.         var index = this.selectionModel.selectedIndex;
  230.         if (index != -1)
  231.           return dataModel.item(index);
  232.       }
  233.       return null;
  234.     },
  235.     set selectedItem(selectedItem) {
  236.       var dataModel = this.dataModel;
  237.       if (dataModel) {
  238.         var index = this.dataModel.indexOf(selectedItem);
  239.         this.selectionModel.selectedIndex = index;
  240.       }
  241.     },
  242.  
  243.     /**
  244.      * Convenience alias for selectionModel.selectedItems
  245.      * @type {!Array<*>}
  246.      */
  247.     get selectedItems() {
  248.       var indexes = this.selectionModel.selectedIndexes;
  249.       var dataModel = this.dataModel;
  250.       if (dataModel) {
  251.         return indexes.map(function(i) {
  252.           return dataModel.item(i);
  253.         });
  254.       }
  255.       return [];
  256.     },
  257.  
  258.     /**
  259.      * The HTML elements representing the items.
  260.      * @type {HTMLCollection}
  261.      */
  262.     get items() {
  263.       return Array.prototype.filter.call(this.children,
  264.                                          this.isItem, this);
  265.     },
  266.  
  267.     /**
  268.      * Returns true if the child is a list item. Subclasses may override this
  269.      * to filter out certain elements.
  270.      * @param {Node} child Child of the list.
  271.      * @return {boolean} True if a list item.
  272.      */
  273.     isItem: function(child) {
  274.       return child.nodeType == Node.ELEMENT_NODE &&
  275.              child != this.beforeFiller_ && child != this.afterFiller_;
  276.     },
  277.  
  278.     batchCount_: 0,
  279.  
  280.     /**
  281.      * When making a lot of updates to the list, the code could be wrapped in
  282.      * the startBatchUpdates and finishBatchUpdates to increase performance. Be
  283.      * sure that the code will not return without calling endBatchUpdates or the
  284.      * list will not be correctly updated.
  285.      */
  286.     startBatchUpdates: function() {
  287.       this.batchCount_++;
  288.     },
  289.  
  290.     /**
  291.      * See startBatchUpdates.
  292.      */
  293.     endBatchUpdates: function() {
  294.       this.batchCount_--;
  295.       if (this.batchCount_ == 0)
  296.         this.redraw();
  297.     },
  298.  
  299.     /**
  300.      * Initializes the element.
  301.      */
  302.     decorate: function() {
  303.       // Add fillers.
  304.       this.beforeFiller_ = this.ownerDocument.createElement('div');
  305.       this.afterFiller_ = this.ownerDocument.createElement('div');
  306.       this.beforeFiller_.className = 'spacer';
  307.       this.afterFiller_.className = 'spacer';
  308.       this.textContent = '';
  309.       this.appendChild(this.beforeFiller_);
  310.       this.appendChild(this.afterFiller_);
  311.  
  312.       var length = this.dataModel ? this.dataModel.length : 0;
  313.       this.selectionModel = new ListSelectionModel(length);
  314.  
  315.       this.addEventListener('dblclick', this.handleDoubleClick_);
  316.       this.addEventListener('mousedown', handleMouseDown);
  317.       this.addEventListener('mouseup', this.handlePointerDownUp_);
  318.       this.addEventListener('keydown', this.handleKeyDown);
  319.       this.addEventListener('focus', this.handleElementFocus_, true);
  320.       this.addEventListener('blur', this.handleElementBlur_, true);
  321.       this.addEventListener('scroll', this.handleScroll.bind(this));
  322.       this.setAttribute('role', 'list');
  323.  
  324.       // Make list focusable
  325.       if (!this.hasAttribute('tabindex'))
  326.         this.tabIndex = 0;
  327.  
  328.       // Try to get an unique id prefix from the id of this element or the
  329.       // nearest ancestor with an id.
  330.       var element = this;
  331.       while (element && !element.id)
  332.         element = element.parentElement;
  333.       if (element && element.id)
  334.         this.uniqueIdPrefix_ = element.id;
  335.       else
  336.         this.uniqueIdPrefix_ = 'list';
  337.  
  338.       // The next id suffix to use when giving each item an unique id.
  339.       this.nextUniqueIdSuffix_ = 0;
  340.     },
  341.  
  342.     /**
  343.      * @param {ListItem=} item The list item to measure.
  344.      * @return {number} The height of the given item. If the fixed height on CSS
  345.      * is set by 'px', uses that value as height. Otherwise, measures the size.
  346.      * @private
  347.      */
  348.     measureItemHeight_: function(item) {
  349.       return this.measureItem(item).height;
  350.     },
  351.  
  352.     /**
  353.      * @return {number} The height of default item, measuring it if necessary.
  354.      * @private
  355.      */
  356.     getDefaultItemHeight_: function() {
  357.       return this.getDefaultItemSize_().height;
  358.     },
  359.  
  360.     /**
  361.      * @param {number} index The index of the item.
  362.      * @return {number} The height of the item, measuring it if necessary.
  363.      */
  364.     getItemHeightByIndex_: function(index) {
  365.       // If |this.fixedHeight_| is true, all the rows have same default height.
  366.       if (this.fixedHeight_)
  367.         return this.getDefaultItemHeight_();
  368.  
  369.       if (this.cachedItemHeights_[index])
  370.         return this.cachedItemHeights_[index];
  371.  
  372.       var item = this.getListItemByIndex(index);
  373.       if (item) {
  374.         var h = this.measureItemHeight_(item);
  375.         this.cachedItemHeights_[index] = h;
  376.         return h;
  377.       }
  378.       return this.getDefaultItemHeight_();
  379.     },
  380.  
  381.     /**
  382.      * @return {{height: number, width: number}} The height and width
  383.      *     of default item, measuring it if necessary.
  384.      * @private
  385.      */
  386.     getDefaultItemSize_: function() {
  387.       if (!this.measured_ || !this.measured_.height) {
  388.         this.measured_ = this.measureItem();
  389.       }
  390.       return this.measured_;
  391.     },
  392.  
  393.     /**
  394.      * Creates an item (dataModel.item(0)) and measures its height. The item is
  395.      * cached instead of creating a new one every time..
  396.      * @param {ListItem=} opt_item The list item to use to do the measuring. If
  397.      *     this is not provided an item will be created based on the first value
  398.      *     in the model.
  399.      * @return {{height: number, marginTop: number, marginBottom:number,
  400.      *     width: number, marginLeft: number, marginRight:number}}
  401.      *     The height and width of the item, taking
  402.      *     margins into account, and the top, bottom, left and right margins
  403.      *     themselves.
  404.      */
  405.     measureItem: function(opt_item) {
  406.       var dataModel = this.dataModel;
  407.       if (!dataModel || !dataModel.length)
  408.         return 0;
  409.       var item = opt_item || this.cachedMeasuredItem_ ||
  410.           this.createItem(dataModel.item(0));
  411.       if (!opt_item) {
  412.         this.cachedMeasuredItem_ = item;
  413.         this.appendChild(item);
  414.       }
  415.  
  416.       var rect = item.getBoundingClientRect();
  417.       var cs = getComputedStyle(item);
  418.       var mt = parseFloat(cs.marginTop);
  419.       var mb = parseFloat(cs.marginBottom);
  420.       var ml = parseFloat(cs.marginLeft);
  421.       var mr = parseFloat(cs.marginRight);
  422.       var h = rect.height;
  423.       var w = rect.width;
  424.       var mh = 0;
  425.       var mv = 0;
  426.  
  427.       // Handle margin collapsing.
  428.       if (mt < 0 && mb < 0) {
  429.         mv = Math.min(mt, mb);
  430.       } else if (mt >= 0 && mb >= 0) {
  431.         mv = Math.max(mt, mb);
  432.       } else {
  433.         mv = mt + mb;
  434.       }
  435.       h += mv;
  436.  
  437.       if (ml < 0 && mr < 0) {
  438.         mh = Math.min(ml, mr);
  439.       } else if (ml >= 0 && mr >= 0) {
  440.         mh = Math.max(ml, mr);
  441.       } else {
  442.         mh = ml + mr;
  443.       }
  444.       w += mh;
  445.  
  446.       if (!opt_item)
  447.         this.removeChild(item);
  448.       return {
  449.           height: Math.max(0, h),
  450.           marginTop: mt, marginBottom: mb,
  451.           width: Math.max(0, w),
  452.           marginLeft: ml, marginRight: mr};
  453.     },
  454.  
  455.     /**
  456.      * Callback for the double click event.
  457.      * @param {Event} e The mouse event object.
  458.      * @private
  459.      */
  460.     handleDoubleClick_: function(e) {
  461.       if (this.disabled)
  462.         return;
  463.  
  464.       var target = e.target;
  465.  
  466.       target = this.getListItemAncestor(target);
  467.       if (target)
  468.         this.activateItemAtIndex(this.getIndexOfListItem(target));
  469.     },
  470.  
  471.     /**
  472.      * Callback for mousedown and mouseup events.
  473.      * @param {Event} e The mouse event object.
  474.      * @private
  475.      */
  476.     handlePointerDownUp_: function(e) {
  477.       if (this.disabled)
  478.         return;
  479.  
  480.       var target = e.target;
  481.  
  482.       // If the target was this element we need to make sure that the user did
  483.       // not click on a border or a scrollbar.
  484.       if (target == this) {
  485.         if (inViewport(target, e))
  486.           this.selectionController_.handlePointerDownUp(e, -1);
  487.         return;
  488.       }
  489.  
  490.       target = this.getListItemAncestor(target);
  491.  
  492.       var index = this.getIndexOfListItem(target);
  493.       this.selectionController_.handlePointerDownUp(e, index);
  494.     },
  495.  
  496.     /**
  497.      * Called when an element in the list is focused. Marks the list as having
  498.      * a focused element, and dispatches an event if it didn't have focus.
  499.      * @param {Event} e The focus event.
  500.      * @private
  501.      */
  502.     handleElementFocus_: function(e) {
  503.       if (!this.hasElementFocus)
  504.         this.hasElementFocus = true;
  505.     },
  506.  
  507.     /**
  508.      * Called when an element in the list is blurred. If focus moves outside
  509.      * the list, marks the list as no longer having focus and dispatches an
  510.      * event.
  511.      * @param {Event} e The blur event.
  512.      * @private
  513.      */
  514.     handleElementBlur_: function(e) {
  515.       if (!this.contains(e.relatedTarget))
  516.         this.hasElementFocus = false;
  517.     },
  518.  
  519.     /**
  520.      * Returns the list item element containing the given element, or null if
  521.      * it doesn't belong to any list item element.
  522.      * @param {HTMLElement} element The element.
  523.      * @return {ListItem} The list item containing |element|, or null.
  524.      */
  525.     getListItemAncestor: function(element) {
  526.       var container = element;
  527.       while (container && container.parentNode != this) {
  528.         container = container.parentNode;
  529.       }
  530.       return container;
  531.     },
  532.  
  533.     /**
  534.      * Handle a keydown event.
  535.      * @param {Event} e The keydown event.
  536.      * @return {boolean} Whether the key event was handled.
  537.      */
  538.     handleKeyDown: function(e) {
  539.       if (this.disabled)
  540.         return;
  541.  
  542.       return this.selectionController_.handleKeyDown(e);
  543.     },
  544.  
  545.     /**
  546.      * Handle a scroll event.
  547.      * @param {Event} e The scroll event.
  548.      */
  549.     handleScroll: function(e) {
  550.       requestAnimationFrame(this.redraw.bind(this));
  551.     },
  552.  
  553.     /**
  554.      * Callback from the selection model. We dispatch {@code change} events
  555.      * when the selection changes.
  556.      * @param {!Event} e Event with change info.
  557.      * @private
  558.      */
  559.     handleOnChange_: function(ce) {
  560.       ce.changes.forEach(function(change) {
  561.         var listItem = this.getListItemByIndex(change.index);
  562.         if (listItem) {
  563.           listItem.selected = change.selected;
  564.           if (change.selected) {
  565.             listItem.setAttribute('aria-posinset', change.index + 1);
  566.             listItem.setAttribute('aria-setsize', this.dataModel.length);
  567.             this.setAttribute('aria-activedescendant', listItem.id);
  568.           } else {
  569.             listItem.removeAttribute('aria-posinset');
  570.             listItem.removeAttribute('aria-setsize');
  571.           }
  572.         }
  573.       }, this);
  574.  
  575.       cr.dispatchSimpleEvent(this, 'change');
  576.     },
  577.  
  578.     /**
  579.      * Handles a change of the lead item from the selection model.
  580.      * @param {Event} pe The property change event.
  581.      * @private
  582.      */
  583.     handleLeadChange_: function(pe) {
  584.       var element;
  585.       if (pe.oldValue != -1) {
  586.         if ((element = this.getListItemByIndex(pe.oldValue)))
  587.           element.lead = false;
  588.       }
  589.  
  590.       if (pe.newValue != -1) {
  591.         if ((element = this.getListItemByIndex(pe.newValue)))
  592.           element.lead = true;
  593.         if (pe.oldValue != pe.newValue) {
  594.           this.scrollIndexIntoView(pe.newValue);
  595.           // If the lead item has a different height than other items, then we
  596.           // may run into a problem that requires a second attempt to scroll
  597.           // it into view. The first scroll attempt will trigger a redraw,
  598.           // which will clear out the list and repopulate it with new items.
  599.           // During the redraw, the list may shrink temporarily, which if the
  600.           // lead item is the last item, will move the scrollTop up since it
  601.           // cannot extend beyond the end of the list. (Sadly, being scrolled to
  602.           // the bottom of the list is not "sticky.") So, we set a timeout to
  603.           // rescroll the list after this all gets sorted out. This is perhaps
  604.           // not the most elegant solution, but no others seem obvious.
  605.           var self = this;
  606.           window.setTimeout(function() {
  607.             self.scrollIndexIntoView(pe.newValue);
  608.           });
  609.         }
  610.       }
  611.     },
  612.  
  613.     /**
  614.      * This handles data model 'permuted' event.
  615.      * this event is dispatched as a part of sort or splice.
  616.      * We need to
  617.      *  - adjust the cache.
  618.      *  - adjust selection.
  619.      *  - redraw. (called in this.endBatchUpdates())
  620.      *  It is important that the cache adjustment happens before selection model
  621.      *  adjustments.
  622.      * @param {Event} e The 'permuted' event.
  623.      */
  624.     handleDataModelPermuted_: function(e) {
  625.       var newCachedItems = {};
  626.       for (var index in this.cachedItems_) {
  627.         if (e.permutation[index] != -1) {
  628.           var newIndex = e.permutation[index];
  629.           newCachedItems[newIndex] = this.cachedItems_[index];
  630.           newCachedItems[newIndex].listIndex = newIndex;
  631.         }
  632.       }
  633.       this.cachedItems_ = newCachedItems;
  634.  
  635.       var newCachedItemHeights = {};
  636.       for (var index in this.cachedItemHeights_) {
  637.         if (e.permutation[index] != -1) {
  638.           newCachedItemHeights[e.permutation[index]] =
  639.               this.cachedItemHeights_[index];
  640.         }
  641.       }
  642.       this.cachedItemHeights_ = newCachedItemHeights;
  643.  
  644.       this.startBatchUpdates();
  645.  
  646.       var sm = this.selectionModel;
  647.       sm.adjustLength(e.newLength);
  648.       sm.adjustToReordering(e.permutation);
  649.  
  650.       this.endBatchUpdates();
  651.     },
  652.  
  653.     handleDataModelChange_: function(e) {
  654.       delete this.cachedItems_[e.index];
  655.       delete this.cachedItemHeights_[e.index];
  656.       this.cachedMeasuredItem_ = null;
  657.  
  658.       if (e.index >= this.firstIndex_ &&
  659.           (e.index < this.lastIndex_ || this.remainingSpace_)) {
  660.         this.redraw();
  661.       }
  662.     },
  663.  
  664.     /**
  665.      * @param {number} index The index of the item.
  666.      * @return {number} The top position of the item inside the list.
  667.      */
  668.     getItemTop: function(index) {
  669.       if (this.fixedHeight_) {
  670.         var itemHeight = this.getDefaultItemHeight_();
  671.         return index * itemHeight;
  672.       } else {
  673.         this.ensureAllItemSizesInCache();
  674.         var top = 0;
  675.         for (var i = 0; i < index; i++) {
  676.           top += this.getItemHeightByIndex_(i);
  677.         }
  678.         return top;
  679.       }
  680.     },
  681.  
  682.     /**
  683.      * @param {number} index The index of the item.
  684.      * @return {number} The row of the item. May vary in the case
  685.      *     of multiple columns.
  686.      */
  687.     getItemRow: function(index) {
  688.       return index;
  689.     },
  690.  
  691.     /**
  692.      * @param {number} row The row.
  693.      * @return {number} The index of the first item in the row.
  694.      */
  695.     getFirstItemInRow: function(row) {
  696.       return row;
  697.     },
  698.  
  699.     /**
  700.      * Ensures that a given index is inside the viewport.
  701.      * @param {number} index The index of the item to scroll into view.
  702.      * @return {boolean} Whether any scrolling was needed.
  703.      */
  704.     scrollIndexIntoView: function(index) {
  705.       var dataModel = this.dataModel;
  706.       if (!dataModel || index < 0 || index >= dataModel.length)
  707.         return false;
  708.  
  709.       var itemHeight = this.getItemHeightByIndex_(index);
  710.       var scrollTop = this.scrollTop;
  711.       var top = this.getItemTop(index);
  712.       var clientHeight = this.clientHeight;
  713.  
  714.       var cs = getComputedStyle(this);
  715.       var paddingY = parseInt(cs.paddingTop, 10) +
  716.                      parseInt(cs.paddingBottom, 10);
  717.       var availableHeight = clientHeight - paddingY;
  718.  
  719.       var self = this;
  720.       // Function to adjust the tops of viewport and row.
  721.       function scrollToAdjustTop() {
  722.           self.scrollTop = top;
  723.           return true;
  724.       };
  725.       // Function to adjust the bottoms of viewport and row.
  726.       function scrollToAdjustBottom() {
  727.           self.scrollTop = top + itemHeight - availableHeight;
  728.           return true;
  729.       };
  730.  
  731.       // Check if the entire of given indexed row can be shown in the viewport.
  732.       if (itemHeight <= availableHeight) {
  733.         if (top < scrollTop)
  734.           return scrollToAdjustTop();
  735.         if (scrollTop + availableHeight < top + itemHeight)
  736.           return scrollToAdjustBottom();
  737.       } else {
  738.         if (scrollTop < top)
  739.           return scrollToAdjustTop();
  740.         if (top + itemHeight < scrollTop + availableHeight)
  741.           return scrollToAdjustBottom();
  742.       }
  743.       return false;
  744.     },
  745.  
  746.     /**
  747.      * @return {!ClientRect} The rect to use for the context menu.
  748.      */
  749.     getRectForContextMenu: function() {
  750.       // TODO(arv): Add trait support so we can share more code between trees
  751.       // and lists.
  752.       var index = this.selectionModel.selectedIndex;
  753.       var el = this.getListItemByIndex(index);
  754.       if (el)
  755.         return el.getBoundingClientRect();
  756.       return this.getBoundingClientRect();
  757.     },
  758.  
  759.     /**
  760.      * Takes a value from the data model and finds the associated list item.
  761.      * @param {*} value The value in the data model that we want to get the list
  762.      *     item for.
  763.      * @return {ListItem} The first found list item or null if not found.
  764.      */
  765.     getListItem: function(value) {
  766.       var dataModel = this.dataModel;
  767.       if (dataModel) {
  768.         var index = dataModel.indexOf(value);
  769.         return this.getListItemByIndex(index);
  770.       }
  771.       return null;
  772.     },
  773.  
  774.     /**
  775.      * Find the list item element at the given index.
  776.      * @param {number} index The index of the list item to get.
  777.      * @return {ListItem} The found list item or null if not found.
  778.      */
  779.     getListItemByIndex: function(index) {
  780.       return this.cachedItems_[index] || null;
  781.     },
  782.  
  783.     /**
  784.      * Find the index of the given list item element.
  785.      * @param {ListItem} item The list item to get the index of.
  786.      * @return {number} The index of the list item, or -1 if not found.
  787.      */
  788.     getIndexOfListItem: function(item) {
  789.       var index = item.listIndex;
  790.       if (this.cachedItems_[index] == item) {
  791.         return index;
  792.       }
  793.       return -1;
  794.     },
  795.  
  796.     /**
  797.      * Creates a new list item.
  798.      * @param {*} value The value to use for the item.
  799.      * @return {!ListItem} The newly created list item.
  800.      */
  801.     createItem: function(value) {
  802.       var item = new this.itemConstructor_(value);
  803.       item.label = value;
  804.       item.id = this.uniqueIdPrefix_ + '-' + this.nextUniqueIdSuffix_++;
  805.       if (typeof item.decorate == 'function')
  806.         item.decorate();
  807.       return item;
  808.     },
  809.  
  810.     /**
  811.      * Creates the selection controller to use internally.
  812.      * @param {cr.ui.ListSelectionModel} sm The underlying selection model.
  813.      * @return {!cr.ui.ListSelectionController} The newly created selection
  814.      *     controller.
  815.      */
  816.     createSelectionController: function(sm) {
  817.       return new ListSelectionController(sm);
  818.     },
  819.  
  820.     /**
  821.      * Return the heights (in pixels) of the top of the given item index within
  822.      * the list, and the height of the given item itself, accounting for the
  823.      * possibility that the lead item may be a different height.
  824.      * @param {number} index The index to find the top height of.
  825.      * @return {{top: number, height: number}} The heights for the given index.
  826.      * @private
  827.      */
  828.     getHeightsForIndex_: function(index) {
  829.       var itemHeight = this.getItemHeightByIndex_(index);
  830.       var top = this.getItemTop(index);
  831.       return {top: top, height: itemHeight};
  832.     },
  833.  
  834.     /**
  835.      * Find the index of the list item containing the given y offset (measured
  836.      * in pixels from the top) within the list. In the case of multiple columns,
  837.      * returns the first index in the row.
  838.      * @param {number} offset The y offset in pixels to get the index of.
  839.      * @return {number} The index of the list item. Returns the list size if
  840.      *     given offset exceeds the height of list.
  841.      * @private
  842.      */
  843.     getIndexForListOffset_: function(offset) {
  844.       var itemHeight = this.getDefaultItemHeight_();
  845.       if (!itemHeight)
  846.         return this.dataModel.length;
  847.  
  848.       if (this.fixedHeight_)
  849.         return this.getFirstItemInRow(Math.floor(offset / itemHeight));
  850.  
  851.       // If offset exceeds the height of list.
  852.       var lastHeight = 0;
  853.       if (this.dataModel.length) {
  854.         var h = this.getHeightsForIndex_(this.dataModel.length - 1);
  855.         lastHeight = h.top + h.height;
  856.       }
  857.       if (lastHeight < offset)
  858.         return this.dataModel.length;
  859.  
  860.       // Estimates index.
  861.       var estimatedIndex = Math.min(Math.floor(offset / itemHeight),
  862.                                     this.dataModel.length - 1);
  863.       var isIncrementing = this.getItemTop(estimatedIndex) < offset;
  864.  
  865.       // Searchs the correct index.
  866.       do {
  867.         var heights = this.getHeightsForIndex_(estimatedIndex);
  868.         var top = heights.top;
  869.         var height = heights.height;
  870.  
  871.         if (top <= offset && offset <= (top + height))
  872.           break;
  873.  
  874.         isIncrementing ? ++estimatedIndex : --estimatedIndex;
  875.       } while (0 < estimatedIndex && estimatedIndex < this.dataModel.length);
  876.  
  877.       return estimatedIndex;
  878.     },
  879.  
  880.     /**
  881.      * Return the number of items that occupy the range of heights between the
  882.      * top of the start item and the end offset.
  883.      * @param {number} startIndex The index of the first visible item.
  884.      * @param {number} endOffset The y offset in pixels of the end of the list.
  885.      * @return {number} The number of list items visible.
  886.      * @private
  887.      */
  888.     countItemsInRange_: function(startIndex, endOffset) {
  889.       var endIndex = this.getIndexForListOffset_(endOffset);
  890.       return endIndex - startIndex + 1;
  891.     },
  892.  
  893.     /**
  894.      * Calculates the number of items fitting in the given viewport.
  895.      * @param {number} scrollTop The scroll top position.
  896.      * @param {number} clientHeight The height of viewport.
  897.      * @return {{first: number, length: number, last: number}} The index of
  898.      *     first item in view port, The number of items, The item past the last.
  899.      */
  900.     getItemsInViewPort: function(scrollTop, clientHeight) {
  901.       if (this.autoExpands_) {
  902.         return {
  903.           first: 0,
  904.           length: this.dataModel.length,
  905.           last: this.dataModel.length};
  906.       } else {
  907.         var firstIndex = this.getIndexForListOffset_(scrollTop);
  908.         var lastIndex = this.getIndexForListOffset_(scrollTop + clientHeight);
  909.  
  910.         return {
  911.           first: firstIndex,
  912.           length: lastIndex - firstIndex + 1,
  913.           last: lastIndex + 1};
  914.       }
  915.     },
  916.  
  917.     /**
  918.      * Merges list items currently existing in the list with items in the range
  919.      * [firstIndex, lastIndex). Removes or adds items if needed.
  920.      * Doesn't delete {@code this.pinnedItem_} if it is present (instead hides
  921.      * it if it is out of the range).
  922.      * @param {number} firstIndex The index of first item, inclusively.
  923.      * @param {number} lastIndex The index of last item, exclusively.
  924.      */
  925.     mergeItems: function(firstIndex, lastIndex) {
  926.       var self = this;
  927.       var dataModel = this.dataModel;
  928.       var currentIndex = firstIndex;
  929.  
  930.       function insert() {
  931.         var dataItem = dataModel.item(currentIndex);
  932.         var newItem = self.cachedItems_[currentIndex] ||
  933.             self.createItem(dataItem);
  934.         newItem.listIndex = currentIndex;
  935.         self.cachedItems_[currentIndex] = newItem;
  936.         self.insertBefore(newItem, item);
  937.         currentIndex++;
  938.       }
  939.  
  940.       function remove() {
  941.         var next = item.nextSibling;
  942.         if (item != self.pinnedItem_)
  943.           self.removeChild(item);
  944.         item = next;
  945.       }
  946.  
  947.       for (var item = this.beforeFiller_.nextSibling;
  948.            item != this.afterFiller_ && currentIndex < lastIndex;) {
  949.         if (!this.isItem(item)) {
  950.           item = item.nextSibling;
  951.           continue;
  952.         }
  953.  
  954.         var index = item.listIndex;
  955.         if (this.cachedItems_[index] != item || index < currentIndex) {
  956.           remove();
  957.         } else if (index == currentIndex) {
  958.           this.cachedItems_[currentIndex] = item;
  959.           item = item.nextSibling;
  960.           currentIndex++;
  961.         } else {  // index > currentIndex
  962.           insert();
  963.         }
  964.       }
  965.  
  966.       while (item != this.afterFiller_) {
  967.         if (this.isItem(item))
  968.           remove();
  969.         else
  970.           item = item.nextSibling;
  971.       }
  972.  
  973.       if (this.pinnedItem_) {
  974.         var index = this.pinnedItem_.listIndex;
  975.         this.pinnedItem_.hidden = index < firstIndex || index >= lastIndex;
  976.         this.cachedItems_[index] = this.pinnedItem_;
  977.         if (index >= lastIndex)
  978.           item = this.pinnedItem_;  // Insert new items before this one.
  979.       }
  980.  
  981.       while (currentIndex < lastIndex)
  982.         insert();
  983.     },
  984.  
  985.     /**
  986.      * Ensures that all the item sizes in the list have been already cached.
  987.      */
  988.     ensureAllItemSizesInCache: function() {
  989.       var measuringIndexes = [];
  990.       var isElementAppended = [];
  991.       for (var y = 0; y < this.dataModel.length; y++) {
  992.         if (!this.cachedItemHeights_[y]) {
  993.           measuringIndexes.push(y);
  994.           isElementAppended.push(false);
  995.         }
  996.       }
  997.  
  998.       var measuringItems = [];
  999.       // Adds temporary elements.
  1000.       for (var y = 0; y < measuringIndexes.length; y++) {
  1001.         var index = measuringIndexes[y];
  1002.         var dataItem = this.dataModel.item(index);
  1003.         var listItem = this.cachedItems_[index] || this.createItem(dataItem);
  1004.         listItem.listIndex = index;
  1005.  
  1006.         // If |listItems| is not on the list, apppends it to the list and sets
  1007.         // the flag.
  1008.         if (!listItem.parentNode) {
  1009.           this.appendChild(listItem);
  1010.           isElementAppended[y] = true;
  1011.         }
  1012.  
  1013.         this.cachedItems_[index] = listItem;
  1014.         measuringItems.push(listItem);
  1015.       }
  1016.  
  1017.       // All mesurings must be placed after adding all the elements, to prevent
  1018.       // performance reducing.
  1019.       for (var y = 0; y < measuringIndexes.length; y++) {
  1020.         var index = measuringIndexes[y];
  1021.         this.cachedItemHeights_[index] =
  1022.             this.measureItemHeight_(measuringItems[y]);
  1023.       }
  1024.  
  1025.       // Removes all the temprary elements.
  1026.       for (var y = 0; y < measuringIndexes.length; y++) {
  1027.         // If the list item has been appended above, removes it.
  1028.         if (isElementAppended[y])
  1029.           this.removeChild(measuringItems[y]);
  1030.       }
  1031.     },
  1032.  
  1033.     /**
  1034.      * Returns the height of after filler in the list.
  1035.      * @param {number} lastIndex The index of item past the last in viewport.
  1036.      * @return {number} The height of after filler.
  1037.      */
  1038.     getAfterFillerHeight: function(lastIndex) {
  1039.       if (this.fixedHeight_) {
  1040.         var itemHeight = this.getDefaultItemHeight_();
  1041.         return (this.dataModel.length - lastIndex) * itemHeight;
  1042.       }
  1043.  
  1044.       var height = 0;
  1045.       for (var i = lastIndex; i < this.dataModel.length; i++)
  1046.         height += this.getItemHeightByIndex_(i);
  1047.       return height;
  1048.     },
  1049.  
  1050.     /**
  1051.      * Redraws the viewport.
  1052.      */
  1053.     redraw: function() {
  1054.       if (this.batchCount_ != 0)
  1055.         return;
  1056.  
  1057.       var dataModel = this.dataModel;
  1058.       if (!dataModel || !this.autoExpands_ && this.clientHeight == 0) {
  1059.         this.cachedItems_ = {};
  1060.         this.firstIndex_ = 0;
  1061.         this.lastIndex_ = 0;
  1062.         this.remainingSpace_ = this.clientHeight != 0;
  1063.         this.mergeItems(0, 0, {}, {});
  1064.         return;
  1065.       }
  1066.  
  1067.       // Save the previous positions before any manipulation of elements.
  1068.       var scrollTop = this.scrollTop;
  1069.       var clientHeight = this.clientHeight;
  1070.  
  1071.       // Store all the item sizes into the cache in advance, to prevent
  1072.       // interleave measuring with mutating dom.
  1073.       if (!this.fixedHeight_)
  1074.         this.ensureAllItemSizesInCache();
  1075.  
  1076.       var autoExpands = this.autoExpands_;
  1077.  
  1078.       var itemsInViewPort = this.getItemsInViewPort(scrollTop, clientHeight);
  1079.       // Draws the hidden rows just above/below the viewport to prevent
  1080.       // flashing in scroll.
  1081.       var firstIndex = Math.max(
  1082.           0,
  1083.           Math.min(dataModel.length - 1, itemsInViewPort.first - 1));
  1084.       var lastIndex = Math.min(itemsInViewPort.last + 1, dataModel.length);
  1085.  
  1086.       var beforeFillerHeight =
  1087.           this.autoExpands ? 0 : this.getItemTop(firstIndex);
  1088.       var afterFillerHeight =
  1089.           this.autoExpands ? 0 : this.getAfterFillerHeight(lastIndex);
  1090.  
  1091.       this.beforeFiller_.style.height = beforeFillerHeight + 'px';
  1092.  
  1093.       var sm = this.selectionModel;
  1094.       var leadIndex = sm.leadIndex;
  1095.  
  1096.       // If the pinned item is hidden and it is not the lead item, then remove
  1097.       // it from cache. Note, that we restore the hidden status to false, since
  1098.       // the item is still in cache, and may be reused.
  1099.       if (this.pinnedItem_ &&
  1100.           this.pinnedItem_ != this.cachedItems_[leadIndex]) {
  1101.         if (this.pinnedItem_.hidden) {
  1102.           this.removeChild(this.pinnedItem_);
  1103.           this.pinnedItem_.hidden = false;
  1104.         }
  1105.         this.pinnedItem_ = undefined;
  1106.       }
  1107.  
  1108.       this.mergeItems(firstIndex, lastIndex);
  1109.  
  1110.       if (!this.pinnedItem_ && this.cachedItems_[leadIndex] &&
  1111.           this.cachedItems_[leadIndex].parentNode == this) {
  1112.         this.pinnedItem_ = this.cachedItems_[leadIndex];
  1113.       }
  1114.  
  1115.       this.afterFiller_.style.height = afterFillerHeight + 'px';
  1116.  
  1117.       // Restores the number of pixels scrolled, since it might be changed while
  1118.       // DOM operations.
  1119.       this.scrollTop = scrollTop;
  1120.  
  1121.       // We don't set the lead or selected properties until after adding all
  1122.       // items, in case they force relayout in response to these events.
  1123.       var listItem = null;
  1124.       if (leadIndex != -1 && this.cachedItems_[leadIndex])
  1125.         this.cachedItems_[leadIndex].lead = true;
  1126.       for (var y = firstIndex; y < lastIndex; y++) {
  1127.         if (sm.getIndexSelected(y))
  1128.           this.cachedItems_[y].selected = true;
  1129.         else if (y != leadIndex)
  1130.           listItem = this.cachedItems_[y];
  1131.       }
  1132.  
  1133.       this.firstIndex_ = firstIndex;
  1134.       this.lastIndex_ = lastIndex;
  1135.  
  1136.       this.remainingSpace_ = itemsInViewPort.last > dataModel.length;
  1137.  
  1138.       // Mesurings must be placed after adding all the elements, to prevent
  1139.       // performance reducing.
  1140.       if (!this.fixedHeight_) {
  1141.         for (var y = firstIndex; y < lastIndex; y++) {
  1142.           this.cachedItemHeights_[y] =
  1143.               this.measureItemHeight_(this.cachedItems_[y]);
  1144.         }
  1145.       }
  1146.     },
  1147.  
  1148.     /**
  1149.      * Restore the lead item that is present in the list but may be updated
  1150.      * in the data model (supposed to be used inside a batch update). Usually
  1151.      * such an item would be recreated in the redraw method. If reinsertion
  1152.      * is undesirable (for instance to prevent losing focus) the item may be
  1153.      * updated and restored. Assumed the listItem relates to the same data item
  1154.      * as the lead item in the begin of the batch update.
  1155.      *
  1156.      * @param {ListItem} leadItem Already existing lead item.
  1157.      */
  1158.     restoreLeadItem: function(leadItem) {
  1159.       delete this.cachedItems_[leadItem.listIndex];
  1160.  
  1161.       leadItem.listIndex = this.selectionModel.leadIndex;
  1162.       this.pinnedItem_ = this.cachedItems_[leadItem.listIndex] = leadItem;
  1163.     },
  1164.  
  1165.     /**
  1166.      * Invalidates list by removing cached items.
  1167.      */
  1168.     invalidate: function() {
  1169.       this.cachedItems_ = {};
  1170.       this.cachedItemSized_ = {};
  1171.     },
  1172.  
  1173.     /**
  1174.      * Redraws a single item.
  1175.      * @param {number} index The row index to redraw.
  1176.      */
  1177.     redrawItem: function(index) {
  1178.       if (index >= this.firstIndex_ &&
  1179.           (index < this.lastIndex_ || this.remainingSpace_)) {
  1180.         delete this.cachedItems_[index];
  1181.         this.redraw();
  1182.       }
  1183.     },
  1184.  
  1185.     /**
  1186.      * Called when a list item is activated, currently only by a double click
  1187.      * event.
  1188.      * @param {number} index The index of the activated item.
  1189.      */
  1190.     activateItemAtIndex: function(index) {
  1191.     },
  1192.  
  1193.     /**
  1194.      * Returns a ListItem for the leadIndex. If the item isn't present in the
  1195.      * list creates it and inserts to the list (may be invisible if it's out of
  1196.      * the visible range).
  1197.      *
  1198.      * Item returned from this method won't be removed until it remains a lead
  1199.      * item or til the data model changes (unlike other items that could be
  1200.      * removed when they go out of the visible range).
  1201.      *
  1202.      * @return {cr.ui.ListItem} The lead item for the list.
  1203.      */
  1204.     ensureLeadItemExists: function() {
  1205.       var index = this.selectionModel.leadIndex;
  1206.       if (index < 0)
  1207.         return null;
  1208.       var cachedItems = this.cachedItems_ || {};
  1209.  
  1210.       var item = cachedItems[index] ||
  1211.                  this.createItem(this.dataModel.item(index));
  1212.       if (this.pinnedItem_ != item && this.pinnedItem_ &&
  1213.           this.pinnedItem_.hidden) {
  1214.         this.removeChild(this.pinnedItem_);
  1215.       }
  1216.       this.pinnedItem_ = item;
  1217.       cachedItems[index] = item;
  1218.       item.listIndex = index;
  1219.       if (item.parentNode == this)
  1220.         return item;
  1221.  
  1222.       if (this.batchCount_ != 0)
  1223.         item.hidden = true;
  1224.  
  1225.       // Item will get to the right place in redraw. Choose place to insert
  1226.       // reducing items reinsertion.
  1227.       if (index <= this.firstIndex_)
  1228.         this.insertBefore(item, this.beforeFiller_.nextSibling);
  1229.       else
  1230.         this.insertBefore(item, this.afterFiller_);
  1231.       this.redraw();
  1232.       return item;
  1233.     },
  1234.  
  1235.     /**
  1236.      * Starts drag selection by reacting 'dragstart' event.
  1237.      * @param {Event} event Event of dragstart.
  1238.      */
  1239.     startDragSelection: function(event) {
  1240.       event.preventDefault();
  1241.       var border = document.createElement('div');
  1242.       border.className = 'drag-selection-border';
  1243.       var rect = this.getBoundingClientRect();
  1244.       var startX = event.clientX - rect.left + this.scrollLeft;
  1245.       var startY = event.clientY - rect.top + this.scrollTop;
  1246.       border.style.left = startX + 'px';
  1247.       border.style.top = startY + 'px';
  1248.       var onMouseMove = function(event) {
  1249.         var inRect = this.getBoundingClientRect();
  1250.         var x = event.clientX - inRect.left + this.scrollLeft;
  1251.         var y = event.clientY - inRect.top + this.scrollTop;
  1252.         border.style.left = Math.min(startX, x) + 'px';
  1253.         border.style.top = Math.min(startY, y) + 'px';
  1254.         border.style.width = Math.abs(startX - x) + 'px';
  1255.         border.style.height = Math.abs(startY - y) + 'px';
  1256.       }.bind(this);
  1257.       var onMouseUp = function() {
  1258.         this.removeChild(border);
  1259.         document.removeEventListener('mousemove', onMouseMove, true);
  1260.         document.removeEventListener('mouseup', onMouseUp, true);
  1261.       }.bind(this);
  1262.       document.addEventListener('mousemove', onMouseMove, true);
  1263.       document.addEventListener('mouseup', onMouseUp, true);
  1264.       this.appendChild(border);
  1265.     },
  1266.   };
  1267.  
  1268.   cr.defineProperty(List, 'disabled', cr.PropertyKind.BOOL_ATTR);
  1269.  
  1270.   /**
  1271.    * Whether the list or one of its descendents has focus. This is necessary
  1272.    * because list items can contain controls that can be focused, and for some
  1273.    * purposes (e.g., styling), the list can still be conceptually focused at
  1274.    * that point even though it doesn't actually have the page focus.
  1275.    */
  1276.   cr.defineProperty(List, 'hasElementFocus', cr.PropertyKind.BOOL_ATTR);
  1277.  
  1278.   /**
  1279.    * Mousedown event handler.
  1280.    * @this {List}
  1281.    * @param {MouseEvent} e The mouse event object.
  1282.    */
  1283.   function handleMouseDown(e) {
  1284.     var listItem = this.getListItemAncestor(e.target);
  1285.     var wasSelected = listItem && listItem.selected;
  1286.     this.handlePointerDownUp_(e);
  1287.  
  1288.     if (e.defaultPrevented || e.button != 0)
  1289.       return;
  1290.  
  1291.     // The following hack is required only if the listItem gets selected.
  1292.     if (!listItem || wasSelected || !listItem.selected)
  1293.       return;
  1294.  
  1295.     // If non-focusable area in a list item is clicked and the item still
  1296.     // contains the focused element, the item did a special focus handling
  1297.     // [1] and we should not focus on the list.
  1298.     //
  1299.     // [1] For example, clicking non-focusable area gives focus on the first
  1300.     // form control in the item.
  1301.     if (!containsFocusableElement(e.target, listItem) &&
  1302.         listItem.contains(listItem.ownerDocument.activeElement)) {
  1303.       e.preventDefault();
  1304.     }
  1305.   }
  1306.  
  1307.   /**
  1308.    * Check if |start| or its ancestor under |root| is focusable.
  1309.    * This is a helper for handleMouseDown.
  1310.    * @param {!Element} start An element which we start to check.
  1311.    * @param {!Element} root An element which we finish to check.
  1312.    * @return {boolean} True if we found a focusable element.
  1313.    */
  1314.   function containsFocusableElement(start, root) {
  1315.     for (var element = start; element && element != root;
  1316.         element = element.parentElement) {
  1317.       if (element.tabIndex >= 0 && !element.disabled)
  1318.         return true;
  1319.     }
  1320.     return false;
  1321.   }
  1322.  
  1323.   return {
  1324.     List: List
  1325.   };
  1326. });
  1327.